'''读写MP3的ID3V2信息的库.现在流行的播放器一般读取MP3文件的ID3V2信息,
   如标题,副标题等.
   公开使用的方法有3个:
   
   1.availableTags() - 返回有效的标签组成的字典.该方法指定了哪些标签(tag)是有效的,
     例如标题是TIT2,所以字符串'TIT2'是一个有效的标签.如果需要增加有效标签,请自行
     修改该方法的代码.
   2.readMP3(filePath) - 读取一个mp3文件的信息,返回一个字典,每一项是'标签':'内容字符串'.
   3.writeMP3(filePath, tags, encoding) - 向一个mp3文件写入tags信息,encoding是编码.
     由于目前只支持gbk与utf16两种格式,故如果encoding不是'gbk',则一律使用utf16.
     
   使用实例请参见'测试方法'.
   MP3文件结构请参见:https://www.cnblogs.com/ranson7zop/p/7655474.html
   
   Last update: 2019/05/30 09:54
   By setycyas
'''

## 定义可用的tag
def availableTags():
  result = {'TIT2':'主标题', 'TIT3':'副标题', 'TPE1':'作者', 
    'TALB':'专辑', 'TRCK':'音轨', 'TYER':'年份', 'TCON':'类型',
    'COMM':'备注'
  }
  return result
  
## 从一个4字节的数组计算标签总长度,是文件开头第7-10个字节表示的数据,算法奇特
def _totalTagsSize(bs):
  result = (bs[0]&0x7F)*0x200000+(bs[1]&0x7F)*0x4000+(bs[2]&0x7F)*0x80+(bs[3]&0x7F)
  return result
  
## 把一个4字节的数组转为整数,低位在前.是每一个tag的第7-10字节
def _fourBytes2Int(bs):
  result = 0
  weight = 1
  for i in range(3, -1, -1):
    result += int(bs[i])*weight
    weight *= 256
  return result
  
## 把一个整数转为4字节数组,低位在前.是每一个tag的第7-10字节
def _int2FourBytes(n):
  result = bytearray([0, 0, 0, 0])
  for i in range(3, -1, -1):
    n, result[i] = divmod(n, 256)
  return bytes(result)

## 给定tag与内容,编码,制作一个字节数组,用于写入文件.编码不是gbk就立即变成utf16.
## 但tag是COMM时,必须是gbk,无视编码,并且前面加上语言提示bytes([0x63, 0x68, 0x69, 0])
def _makeATag(tag, data, encoding):
  # 生成tag字节数组,都是ascii编码
  tagBytes = tag.encode()
  # 设定encoding
  encoding = encoding.lower()
  if (encoding != 'gbk'):
    encoding = 'utf16'
  if (tag == 'COMM'):
    encoding = 'gbk'
  # 内容字节数组生成,注意前面加一个字节的标志,0表示gkb,1表示utf16.utf16最后要加3个0
  dataBytes = data.encode(encoding, 'replace')
  if (tag == 'COMM'):
    dataBytes = bytes([0x63, 0x68, 0x69, 0])+dataBytes
  if (encoding == 'gbk'):
    dataBytes = bytes([0])+dataBytes
  else:
    dataBytes = bytes([1])+dataBytes+bytes([0, 0, 0])
  # 生成表示内容长度的4字节数组
  lenBytes = _int2FourBytes(len(dataBytes))
  # 返回
  return tagBytes+lenBytes+bytes([0, 0])+dataBytes
  
## 读取已经打开的文件指针的一个tag,注意并不会考虑文件的当前指针位置,
## 需要使用该方法前手动设定好指针.返回[tag,内容],若读取失败,返回空None
## 注意即使读取失败,依然会移动文件指针!
def _readATag(f):
  # 读取tag
  try:
    tag = f.read(4).decode()
  except:
    return None
  # 读取内容长度,不计编码位,所以-1
  dataSize = _fourBytes2Int(f.read(4))-1
  # 跳过2个空字符
  f.read(2)
  # 读取编码位
  if (f.read(1)[0] == 0):
    encoding = 'gbk'
  else:
    encoding = 'utf16'
  # 读取内容
  dataBytes = f.read(dataSize)
  if (encoding == 'utf16'):
    dataBytes = dataBytes[:-3]
  if (tag == 'COMM'):
    dataBytes = dataBytes[4:]
  try:
    data = dataBytes.decode(encoding)
  except:
    return None
  return [tag, data]
  
# 读取一个mp3文件的信息,若读取失败(例如不是mp3文件),返回None
def readMP3(filePath):
  result = {}
  aviTagList = availableTags()
  with open(filePath, 'rb+') as f:
    # 读取前3个字符判断是否mp3
    if (f.read(3).decode() != 'ID3'):
      return None
    # 跳过7个字符
    f.read(7)
    # 不断读取tag,直到读取失败或tag不在可用列表中
    while(True):
      item = _readATag(f)
      if (not (item)):
        break
      if (not (item[0] in aviTagList)):
        break
      result[item[0]] = item[1]
  return result

# 写入一个MP3文件的信息,成功返回写入数据的字典(一般与tag一样),失败返回None  
def writeMP3(filePath, tags, encoding = 'utf16'):
  result = {}
  aviTagList = availableTags()
  with open(filePath, 'rb+') as f:
    # 读取前3个字符判断是否mp3
    if (f.read(3).decode() != 'ID3'):
      return None
    # 跳过3个字符
    f.read(3)
    # 计算信息最大长度,若信息量超过这个该长度,返回None,不足该长度则在后面补0
    tagsSize = _totalTagsSize(f.read(4))
    # 准备即将写入的字节数组bytesToWrite
    bytesToWrite = bytes(0)
    for tag in tags:
      if (not (tag in aviTagList)):
        continue
      else:
        result[tag] = tags[tag]
      bytesToWrite += _makeATag(tag, tags[tag], encoding)
    # 判别写入内容长度,过长则失败,过短则补0
    if (len(bytesToWrite) > tagsSize):
      return None
    else:
      bytesToWrite += bytes(tagsSize-len(bytesToWrite))
    # 写入
    f.seek(10, 0)
    f.write(bytesToWrite)
  return result

####################################################
####### 下面开始是测试与测试方法  #########
####################################################

# 测试方法
def _readATag_Test():
  filePath = r'D:\software\python351\MyPython\myApp\_From20180405\MP3ReadWrite\mp3Test_gbk.mp3'
  with open(filePath, 'rb+') as f:
    f.seek(10, 0)
    print(_readATag(f))
    print(_readATag(f))
  return
  
# 测试方法
def _readMP3_Test(): 
  filePath = r'D:\software\python351\MyPython\myApp\_From20180405\MP3ReadWrite\mp3Test_gbk_copy.mp3'
  print(readMP3(filePath))
  
# 测试方法
def _writeMP3_Test():
  filePath = r'D:\software\python351\MyPython\myApp\_From20180405\MP3ReadWrite\mp3Test_gbk_copy.mp3'
  tags = {
    'TIT2':'测试TIT2あ', 'TIT3':'测试副标题栞', 'TPE1':'测试作者紗耶香', 
    'TPE2':'Test_tpe2',
    'TALB':'测试专辑メセル', 'TRCK':'5/10', 'TYER':'2019', 'TCON':'测试TCON',
    'COMM':'测试COMMセル'
  }
  print(writeMP3(filePath, tags, 'gbk'))
 
if __name__ == '__main__':
  _writeMP3_Test()
  